/*
 * cron4j - A pure Java cron-like scheduler
 * 
 * Copyright (C) 2007-2010 Carlo Pelliccia (www.sauronsoftware.it)
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version
 * 2.1, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License 2.1 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License version 2.1 along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 */
package com.github.jspxnet.cron4j;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.*;

/**
 * <p>
 * A built-in {@link Task} implementation which can be used to run an external
 * process.
 * </p>
 * 
 * @author Carlo Pelliccia
 * @since 2.1
 */
@Slf4j
@Setter
@Getter
public class ProcessTask extends Task {

	/**
	 * The command to launch.
     * -- GETTER --
     *  Returns the command executed by this task.
     * -- SETTER --
     *  Sets the command executed by this task.
     *
     */
	private String[] command;

	/**
	 * Environment variables for the spawned process, in the form
	 * <em>name=value</em>. If null the process will inherit the current JVM
	 * environment variables.
     * -- GETTER --
     *  Returns the environment variables, in the <em>name=value</em> form, used
     *  by the task to run its process. If null the process will inherit the
     *  current JVM environment variables.
     * -- SETTER --
     *  Sets the environment variables, in the <em>name=value</em> form, used by
     *  the task to run its process. If null the process will inherit the current
     *  JVM environment variables.
     */
	private String[] envs;

	/**
	 * Working directory for the spawned process. If null the process will
	 * inherit the current JVM working directory.
     * -- GETTER --
     *  Resturns the working directory for the spawned process. If null the
     *  process will inherit the current JVM working directory.
     * -- SETTER --
     *  Sets the working directory for the spawned process. If null the process
     */
	private File directory;

	/**
	 * Standard input file (optional).
     * -- GETTER --
     *  Returns the standard input file (optional). If supplied, the standard
     *  input channel of the spawned process will be read from the given file.
     * -- SETTER --
     *  Sets the standard input file (optional). If supplied, the standard input
     *  channel of the spawned process will be read from the given file.
     */
	private File stdinFile = null;

	/**
	 * Standard output file (optional).
     * -- GETTER --
     *  Returns the standard output file (optional). If supplied, the standard
     *  output channel of the spawned process will be written in the given file.
     * -- SETTER --
     *  Sets the standard output file (optional). If supplied, the standard
     *  output channel of the spawned process will be written in the given file.
    */
	private File stdoutFile = null;

	/**
	 * Standard error file (optional).
     * -- GETTER --
     *  Returns the standard error file (optional). If supplied, the standard
     *  error channel of the spawned process will be written in the given file.
     * -- SETTER --
     *  Sets the standard error file (optional). If supplied, the standard error
     *  channel of the spawned process will be written in the given file.
     */
	private File stderrFile = null;


	/**
	 *
	 * @param command  The command to launch and its arguments.
	 * @param envs  Environment variables for the spawned process, in the form
	 * 	             <em>name=value</em>. If null the process will inherit the
	 * 	             current JVM environment variables.
	 * @param directory Working directory for the spawned process. If null the process
	 * 	 *            will inherit the current JVM working directory.
	 * @param id 放个id号,让外部来生成
	 */
	public ProcessTask(String[] command, String[] envs, File directory,String id) {
		super(id);
		this.command = command;
		this.envs = envs;
		this.directory = directory;
	}



	/**
	 * Returns true.
	 */
	@Override
	public boolean canBeStopped() {
		return true;
	}

    /**
	 * Implements {@link Task#execute(TaskExecutionContext)}. Runs the given
	 * command as a separate process and waits for its end.
	 */
	@Override
	public void execute(TaskExecutionContext context) throws RuntimeException {
		Process p;
		try {
			p = exec();
		} catch (IOException e) {
			throw new RuntimeException(toString() + " cannot be started", e);
		}
		InputStream in = buildInputStream(stdinFile);
		OutputStream out = buildOutputStream(stdoutFile);
		OutputStream err = buildOutputStream(stderrFile);
		if (in != null) {
			StreamBridge b = new StreamBridge(in, p.getOutputStream());
			b.start();
		}
		if (out != null) {
			StreamBridge b = new StreamBridge(p.getInputStream(), out);
			b.start();
		}
		if (err != null) {
			StreamBridge b = new StreamBridge(p.getErrorStream(), err);
			b.start();
		}
		int r;
		try {
			r = p.waitFor();
		} catch (InterruptedException e) {
			throw new RuntimeException(toString() + " has been interrupted");
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (Throwable e) {
					//...
				}
			}
			if (out != null) {
				try {
					out.close();
				} catch (Throwable e) {
					//...
				}
			}
			if (err != null) {
				try {
					err.close();
				} catch (Throwable e) {
					//...
				}
			}
			p.destroy();
		}
		if (r != 0) {
			throw new RuntimeException(toString() + " returns with error code " + r);
		}
	}

	/**
	 * Executes the command.
	 * 
	 * @return The launched Process.
	 * @throws IOException
	 *             If an I/O error occurs.
	 */
	private Process exec() throws IOException {
		Runtime rt = Runtime.getRuntime();
		Process p;
		try {
			// java 1.3+
			p = rt.exec(command, envs, directory);
		} catch (NoSuchMethodError e) {
			// java 1.2
			p = rt.exec(command, envs);
		}
		return p;
	}

	/**
	 * Prepares an {@link InputStream} on a file and returns it.
	 * 
	 * @param file
	 *            The file.
	 * @return The stream, or null if the file is not found.
	 */
	private InputStream buildInputStream(File file) {
		if (file != null) {
			try {
				return new FileInputStream(file);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				return null;
			}
		}
		return null;
	}

	/**
	 * Prepares an {@link OutputStream} on a file and returns it.
	 * 
	 * @param file
	 *            The file.
	 * @return The stream, or null if the file is not found.
	 */
	private OutputStream buildOutputStream(File file) {
		if (file != null) {
			try {
				return new FileOutputStream(file);
			} catch (FileNotFoundException e) {
				log.error("buildOutputStream file:{}", file,e);
				return null;
			}
		}
		return null;
	}

	/**
	 * Prints in the returned string the elements contained in the given string
	 * array.
	 * 
	 * @param arr
	 *            The array.
	 * @return A string representing the supplied array contents.
	 */
	private static String listStrings(String[] arr) {
		if (arr == null) {
			return "null";
		} else {
			StringBuilder b = new StringBuilder();
			b.append('[');
			for (int i = 0; i < arr.length; i++) {
				if (i > 0) {
					b.append(",");
				}
				b.append(arr[i]);
			}
			b.append(']');
			return b.toString();
		}
	}

	/**
	 * Overrides {@link Object#toString()}.
	 */
	@Override
	public String toString() {
		return "Task[" +
				"cmd=" +
				ProcessTask.listStrings(command) +
				",env=" +
				ProcessTask.listStrings(envs) +
				"," +
				"dir=" +
				directory +
				"]";
	}

}
